What are little modules made of ?

If you know how to write machine code on the Arc you usually want to put it to some good use; or at least try it out on things. There are a number of ways of doing this : It is the latter of these which causes people the most problems. However, so long as you know what you are doing it doesn't have to be like that.

The anatomy of a module

Modules consist of x parts, of which only the first is actually required.
  1. Module header - Require
    This declares the module to the operating system and describes what kinds of call it supports and how it talks to programs.
  2. Start code
    This is called when the module is RMRun. Usually this is used to start module tasks.
  3. Initialisation code
    This is called when the module is initialised. Usually this sets up any workspace the module needs and claims vectors as it requires.
  4. Finalisation code
    This is called when the module is closed down. This should release any vectors and workspace it has claimed.
  5. The service handler
    The service handler is called whenever a special event is generated of which modules may need to be notified. This is mostly used to trap when filing systems start up or close down, and other changes to the OS. Most people won't need to worry about this.
  6. Command table
    This describes the OSCLI commands that a command may take, and what it should do with them. Help and configuration options are also in this category.
  7. SWI block
    This controls the SWIs which the module supplies and allows it to decode the textual SWI names.

The Module Header

The module header consists of word offsets from the start of the module. Mostly this is because everything in a module should be relocatable and self contained (about which, more later). These offsets describe which of the other functions the module supports. They are :
Offset Contents
---------------------------------------------
00     Start code, or 0 if none
04     Initialisation code, or 0 if none
08     Finalisation code, or 0 if none
0C     Service handler code, or 0 if none
10     Title string (see below)
14     Help string (see below)
18     Command table, or 0 if none
1C     SWI base number, or anything else
20     SWI handler code, or anything else
24     SWI decoding table, or anything else
28     SWI decoding code, or anything else
The last four words are somewhat strange in that they may or may not be present. If they aren't present then data or code may be in that location instead. A good rule of thumb is that if any of the four entries would be rubbish as the SWI data it is considered that the section is invalid.

Title and help strings

The title string is the name of the module as it is known to the OS; that is, it is the name you will need to use when you do *RMKill, and you see when you type *Modules. Therefore, it should not include any spaces and should be descriptive of the module.

The help string is displayed when you do *Help <module name> or *Help Modules. It should include the version number and date string for then module and should be tabbed in so that it lines up with the other module names. Most people like to stick their name after the date string so that they get some credit for the module. You should try to stick to the same kind of style as the standard Acorn modules.

Workspace for modules

The idea behind modules is that they are self contained and they should be able to be saved, reset and loaded at almost any time. Whilst this isn't true of some modules (eg ADFS, FilterManager), most modules should be able to cope with these things. Therefore, you should not store any data inside the module which you could write to.

This is very important because it means that if (at some point) it becomes viable to blow a group of modules into a ROM it would be nice if they would work by not writing to themselves. This isn't, of course, the main reason, but it is the one I live in hope of....

Because of this, modules should claim workspace in RMA to use for all their calls in their initialisation code, and release it in their finalisation code. When the init code is called it is passed a single word piece of workspace which it may use to store private data. Usually this is where the workspace pointer is stored, but regardless of the value, it will be passed on to all the other routines in r12.

The command table

The command table is really quite simple. They are of the form... ... which can be repeated any number of times, terminated by a zero byte instead of the command name.

The Module

Ok, that's got most of the details over and done with, and even if you haven't understood all of it, I hope you'll stick with me whilst I demonstrate what you can do with it....

Firstly, we need to decide what we are going to do with our nice module. Because I'm lazy and I don't want to do anything difficult (and the fact that I just happen to have such a program lying around), I've decided that I want to write a *Command which redirects all output to a file instead of the screen, sort of like redirection.

Next, we need to decide what assembler we want to use. Since it is available on all computers, I'm going to use the built-in BASIC assembler. I don't use this for preference because modules are not easy to write for a group of reasons. However, I'll say more about that later...

What I want to do is to implement a command called WCDivert (Write Character dirverter) which can take one parameter (the filename) to start a diversion, or none to close the diversion.

The Header in BASIC

The header is very simple to put together really - we just need to put the offsets to the module in. We need to record the file handle we are writing to so that we can release it later, we need some workspace to store it in, and we need a command to activate the redirection.
   EQUD 0         ; Start offset (ie none)
   EQUD init      ; Initialisation offset
   EQUD final     ; Finalisation offset
   EQUD 0         ; Service request offset (ie none)
   EQUD title     ; Title string offset
   EQUD help      ; Help string offset
   EQUD commands  ; Help and command keyword table offset
The title and header strings are quite easy to do :
.title
   EQUS "WCDivert"+CHR$0  ; *Modules string
.help
   EQUS "WCDivert"+CHR$9+"1.00 ("+MID$(TIME$,5,11)+") © Justin Fletcher"
   EQUB 0                 ; *Help Modules string
   ALIGN                  ; I'm not sure what's coming next ;-)
Notice that I have used a semi-colon in the smiley on the last line. This is important not to the module, but to BASIC, because it thinks that any colon on a line (even in a comment) is a new command.

The command table

We only need one command, so the command table is pretty simple, but you could keep extending it as far as you wished.
.commands
   EQUS "WCCapture"+CHR$(0); Command name
   ALIGN
   EQUD wccapture          ; Code to call
   EQUB &00                ; Flags - minimum number of params
   EQUB &00                ;       - erm... not a clue...
   EQUB &01                ;       - maximum number of params
   EQUB &00                ;       - normal command (I think)
   EQUD wcsyntax           ; Syntax pointer
   EQUD wchelp             ; Help pointer
   EQUB 0:ALIGN            ; Finish the command table
I've split up the flags word there so that it is easier to see what is going on, but usually I retain it in the double word format.

The syntax and help strings are simply zero-terminated strings, so they are quite easy to set up :

.wcsyntax
   EQUS "Syntax: *WCCapture [<filename>]"
   EQUB 0
.wchelp
   EQUS "*WCCapture is used to start (giving a filename), or end a capture "
   EQUS "session. Fun, init ? "
   EQUB 0
   ALIGN
Not much of any real interest yet, but you should be getting the idea that things are not as difficult as they look - I hope...

Initialisation and finalisation

Unfortunately, from now on things get a bit harder because we have to do that difficult business of claiming and releasing memory. However, with a little bit of thought it is quite simple...

Initialisation

The init code needs to claim the workspace the module will need and initialise it so that it is in the 'normal' state. When called, r12 contains the location of the private word for the module. This is where we are going to store the workspace pointer. We only need 4 bytes, but we should still use the workspace and not store anything in the module itself.
.init
   STMFD   (sp)!,{r0-r3,link}            ; Stack registers
   MOV     r0,#6                         ; OS_Module code for claim
   MOV     r3,#4                         ; How many bytes ?
   SWI     "XOS_Module"                  ; Claim private workspace
   ADDVS   sp,sp,#4                      ; if an error occurred then return
   LDMVSFD (sp),{r1-r4,pc}               ; return with the error pointer
   STR     r2,[r12]                      ; store our WS in private word
   MOV     r0,#0                         ; zero byte to initialise
   STR     r0,[r2]                       ; store in workspace
   LDMFD   (sp)!,{r0-r3,pc}              ; Return from call safely
Notice the use of the X version of the SWI call when we claim the workspace; this is because we should return errors to the OS in the standard way (r0 -> error block), and therefore we skip r0 when we restore the registers and return.

Finalisation

Finalisation is a bit more difficult, because we have to check whether the output is currently diverted and restore it, as otherwise you'll be stuck with diverted output which you can't restore.

However, because we are going to need to release the block later, we might as well just call that code.

.final
   STMFD   (sp)!,{r0,r2,r4,link}         ; Stack registers
   LDR     r4,[r12]                      ; get the file handle
   CMP     r4,#0                         ; is it open ?
   BLEQ    doclose                       ; if so, close and restore output
   LDR     r2,[r12]                      ; read workspace pointer from private word
   MOV     r0,#7                         ; OS_Module release block
   SWI     "XOS_Module"                  ; release the workspace
   STRVS   r0,[sp]                       ; if error, store block on stack
   LDMFD   (sp)!,{r0,r2,r4,pc}           ; Return from call
doclose is the routine I'm going to use which will close the file and release the Wrch (Write Character) vector. I've decided that I'm going to pass the file handle to the doclose routine in r4.

Notice the way in which I have returned the error to the OS if it exists. I'm not overly keen on storing values on the stack like this, but it doesn't half make the code look neat. Because r0 is the lowest register stacked we can store the new value over the old one and then when it returns, because the flags are not restored the V flag is still set.

The WCCapture command

The command itself is relatively simple as all it needs to do is to enable or disable the vector itself (oh, and open or close the files). For simplicities sake, I'm not going to return errors to the user if the file is already open, or has already been closed.
.wccapture
   STMFD   (sp)!,{r0-r5,link}            ; Stack registers
   LDR     r12,[r12]                     ; read workspace from private word
We have to read the workspace pointer from out of the private word first, otherwise we'll end up writing to the private word itself.
   CMP     r1,#1                         ; is this a start ?
   BNE     closecapture                  ; if not, jump to close code

   LDR     r4,[r12]                      ; read the current handle
   CMP     r4,#0                         ; are we already diverting output ?
   BNE     wcexit                        ; if so, exit (no error)
We know that this is an open request and that we aren't redirecting at the moment, so we need to open the file and claim the vector.
   MOV     r1,r0                         ; r1 -> string
   MOV     r0,#&8c                       ; code to open file with errors
   SWI     "XOS_Find"                    ; open file
   
   BVS     wcexit                        ; if errors exit gracefully
   STR     r0,[r12]                      ; store the handle in workspace ;-)
   
   ADR     r1,intercept                  ; the code to use
   MOV     r0,#3                         ; the Wrch vector
   MOV     r2,r12                        ; pass to routine in r12
   SWI     "OS_Claim"                    ; claim vector
We can now safely exit, and if there is an error we should return it.
.wcexit
   STRVS   r0,[sp]                       ; if error, store block on stack
   LDMFD   (sp)!,{r0-r5,pc}              ; Return from call
... and we need some code to handle if we are closing the file instead ...
.closecapture
   LDR     r4,[r12]                      ; read file handle
   CMP     r4,#0                         ; are we actually diverting output ?
   BEQ     wcexit                        ; if not, exit (no error)
   BL      doclose                       ; do the close and release
   B       wcexit                        ; exit nicely

The doclose routine

The doclose routine is quite simple really, and simply closes the file and releases the vector. Remember that I decreed that r4 would be the file handle when I wrote the finalisation routine
.doclose
   STMFD   (sp)!,{r0-r2,link}            ; Stack registers
   ADR     r1,intercept                  ; the code to release
   MOV     r0,#3                         ; WRCH vector
   MOV     r2,r12                        ; r12 as passed to the routine
   SWI     "OS_Release"                  ; claim the WRCH vector
   MOV     r0,#0                         ; code for closing files
   MOV     r1,r4                         ; the file handle we're closing
   SWI     "XOS_Find"                    ; close the file
   MOV     r1,#0                         ; mark as closed
   STR     r1,[r12]                      ; store in the workspace
   LDMFD   (sp)!,{r0-r2,pc}              ; Return from call
Notice that I've marked the file as closed in the workspace - otherwise when we try to *RMKill the module it will attempt to close files which aren't open and make a mess all over the floor <grin>.

The WrchV intercept code

The code to actually do the job of putting stuff on the disc is the simplest of all...
.intercept
   STMFD   (sp)!,{r0-r1,link}            ; Stack registers
   LDR     r1,[r12]                      ; read the file handle from WS
   SWI     "XOS_BPut"                    ; put the byte
   LDMFD   (sp)!,{r0-r1,link}            ; restore registers
                                         ; intercept the vector
   LDMFD   (sp)!,{pc}^                   ; Return from call with flags
I don't want any output to the screen, so I've restored the link register back into the link register and pulled the old pc off the stack along with all the flags. Restoring the flags on this occassion is very important because you must retain the same standards as the original call if you intercept a vector. If you wanted you could change the code so it didn't intercept the vector, but instead just wrote to a file as well as to the screen; however there is already a command to do that - *Spool !

The BASIC code

Unfortunately, that's not it. Although we've finished with the code itself, we need to stick it in a BASIC program. If you know ARM code then I'll assume you know how to do this, so I shalln't explain all the horrible stuff you need.

Header

You would need to stick this at the top ...
REM >MakeWCC
DIM m% 1024
sp=13:link=14:pc=15
FORI=4TO6 STEP2
P%=0:O%=m%
[OPTI

Footer

... and this bit at the bottom ...
]
NEXT I
SYS "OS_File",10,"$.WCC",&FFA,,m%,O%
PRINT "Module saved"
And that's about it really... It's not too difficult - the most difficult bit was the code to intercept WrchV, and not the making it into a module.

!JFPatch

[JFPatch] I mentioned before that Basic was not my language of choice for writing modules in. That was a white lie really because I do, sort of, write modules in Basic assembler. I actually wrote a program called JFPatch to make the job of writing modules. This makes life easier for many reasons, not least in that you can describe the module header in textual terms and it will build the correct Basic assembler code for you. For more information about JFPatch, or to download it, click here.

Downloading

The following are for comparison with the original basic version
This page is maintained by Justin Fletcher (Gerph@essex.ac.uk).
Last modified on 10th June 1996.